/*
 * Copyright 2010 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.websocket;

import org.eclipse.jetty.util.Utf8StringBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

/**
 * Various shared utility methods.
 * 
 * TODO: find a better place.
 */
class Util {

  public static final Charset UTF8 = Charset.forName("UTF8");

  public static Map<String, String> readHeaderFields(InputStream in)
      throws IOException {
    String line;
    Map<String, String> headerFields = new HashMap<String, String>();
    while (true) {
      line = Util.readUtf8Line(in);
      if (line.length() == 0) {
        // an empty line terminates the headers
        break;
      }
      int colonIdx = line.indexOf(':');
      if (colonIdx < 0) {
        throw new IOException("Malformed header in HTTP response");
      }
      int valueIdx = colonIdx + 1;
      while (valueIdx < line.length() && line.charAt(valueIdx) == ' ') {
        valueIdx++;
      }
      String key = line.substring(0, colonIdx).toLowerCase(Locale.ENGLISH);
      if (headerFields.containsKey(key)) {
        throw new IOException("Duplicated header field '" + key + "'");
      }
      headerFields.put(key, line.substring(valueIdx));
    }
    return headerFields;
  }

  public static String readUtf8Line(InputStream in) throws IOException {
    Utf8StringBuilder buf = new Utf8StringBuilder();
    int c;
    int eolChars = 0;
    // TODO(jat): timeout on read
    while (eolChars < 2 && (c = in.read()) >= 0) {
      switch (eolChars) {
        case 0:
          if (c == 13) {
            eolChars++;
            continue;
          }
          break;
        case 1:
          if (c == 10) {
            eolChars++;
            continue;
          } else {
            eolChars = 0;
            buf.append((byte) 13);
          }
          break;
      }
      buf.append((byte) c);
    }
    return buf.toString();
  }

  static ServerSocket getBoundServerSocket(int bindPort)
      throws IOException, SocketException {
    ServerSocket socket = new ServerSocket();
    SocketAddress addr = new InetSocketAddress(bindPort);
    socket.setReuseAddress(true);
    socket.bind(addr);
    return socket;
  }

  /**
   * Returns the number of bytes a given length will take to encode.
   *
   * @param length
   * @return the number of bytes for this length
   */
  static long numberOfLengthBytes(long length) {
    if (length < 126) {
      return 1;
    } else if (length < 65536) {
      return 3;
    } else {
      return 9;
    }
  }

  /**
   * Read a byte from the specified stream.
   * @param istr TODO(jat)
   *
   * @return read byte as an integer
   * @throws IOException on EOF or other errors
   */
  static int readByte(InputStream istr) throws IOException {
    int val = istr.read();
    if (val < 0) {
      throw new IOException("EOF on read");
    }
    return val;
  }

  /**
   * Read from the buffered input stream until the supplied buffer is completely
   * read, or until an error occurs.
   * 
   * @param buf
   * @param istr input stream to read from
   * @throws IOException
   */
  static void readFully(byte[] buf, InputStream istr)
      throws IOException {
    int len = buf.length;
    int ofs = 0;
    while (ofs < len) {
      int n = istr.read(buf, ofs, len - ofs);
      if (n < 0) {
        throw new IOException("EOF during metadata");
      }
      ofs += n;
    }
  }

  /**
   * Read a length encoded as a 7/16/63-bit value.
   *
   * @param firstByte the first byte of the sequence
   * @param istr input stream to read further bytes from
   * @return decoded length
   * @throws IOException
   */
  static long readLength(int firstByte, InputStream istr) throws IOException {
    long length = firstByte & DraftWebSocket.LENGTH_MASK;
    switch (firstByte) {
      case DraftWebSocket.LENGTH_16:
        length = (Util.readByte(istr) << 8) | Util.readByte(istr);
        break;
      case DraftWebSocket.LENGTH_63:
        length = (Util.readByte(istr) << 54) | (Util.readByte(istr) << 48)
            | (Util.readByte(istr) << 40) | (Util.readByte(istr) << 32)
            | (Util.readByte(istr) << 24) | (Util.readByte(istr) << 16)
            | (Util.readByte(istr) << 8) | Util.readByte(istr);
        break;
    }
    return length;
  }

  /**
   * Read a length encoded according to v76.
   *
   * @param istr input stream to read further bytes from
   * @return decoded length
   * @throws IOException
   */
  static long readV76Length(InputStream istr) throws IOException {
    long length = 0;
    int b;
    do {
      b = readByte(istr);
      length = (length << 7) | (b & 127);
    } while ((b & 128) != 0);
    return length;
  }

  static void writeLength(OutputStream ostr, long length,
      boolean topBit) throws IOException {
    if (length < 126) {
      ostr.write((topBit ? 0x80 : 0) | (int) length & 127);
    } else if (length < 65536) {
      ostr.write((topBit ? 0x80 : 0) | DraftWebSocket.LENGTH_16);
      ostr.write((int) (length >> 8) & 255);
      ostr.write((int) length & 255);
    } else {
      ostr.write((topBit ? 0x80 : 0) | DraftWebSocket.LENGTH_63);
      ostr.write((int) (length >> 54) & 127);
      ostr.write((int) (length >> 48) & 255);
      ostr.write((int) (length >> 40) & 255);
      ostr.write((int) (length >> 32) & 255);
      ostr.write((int) (length >> 24) & 255);
      ostr.write((int) (length >> 16) & 255);
      ostr.write((int) (length >> 8) & 255);
      ostr.write((int) length & 255);
    }
  }

  static void writeLong(OutputStream out, long val)
     throws IOException {
    out.write((int) ((val >> 56) & 255));
    out.write((int) ((val >> 48) & 255));
    out.write((int) ((val >> 40) & 255));
    out.write((int) ((val >> 32) & 255));
    out.write((int) ((val >> 24) & 255));
    out.write((int) ((val >> 16) & 255));
    out.write((int) ((val >> 8) & 255));
    out.write((int) (val & 255));
  }

  static void writeUnsignedShort(OutputStream out, int val)
     throws IOException {
    out.write((val >> 8) & 255);
    out.write(val & 255);
  }

  static void writeV76Length(OutputStream ostr, long length)
      throws IOException {
    byte[] buf = new byte[9];
    int i;
    for (i = 0; (i != 0 || length != 0) && i < 9; ++i) {
      buf[i] = (byte) (length & 127);
      length = length >> 7;
    }
    int flag = 0;
    while (i >= 0) {
      ostr.write(buf[i] | flag);
      flag = 0x80;
    }
  }
}
